#include "YAML.h"

std::string NahidaProject::YAML::YAMLTypeToString(YAMLType type) {
	switch (type) {
	case YAMLType::intType:
		return "int";
	case YAMLType::doubleType:
		return "double";
	case YAMLType::stringType:
		return "string";
	case YAMLType::arrayType:
		return "array";
	case YAMLType::boolType:
		return "bool";
	case YAMLType::objectType:
		return "object";
	}
	return "None";
}

NahidaProject::YAML::TypedValue NahidaProject::YAML::TypedValue::operator[](size_t index){
	if (type != YAMLType::arrayType){
		std::cerr << "ERROR: Attempting to cast TypedValue of type " << YAMLTypeToString(type) << " to array.";
		return TypedValue();
	}
	return static_cast<std::vector<TypedValue>*>(value)->at(index);
}

NahidaProject::YAML::TypedValue NahidaProject::YAML::TypedValue::operator[](std::string name){
	if (type != YAMLType::objectType){
		std::cerr << "ERROR: Attempting to cast TypedValue of type " << YAMLTypeToString(type) << " to object.";
		return TypedValue();
	}
	return static_cast<std::unordered_map<std::string, TypedValue>*>(value)->at(name);
}


// Counts the number of spaces set at the beginning of a string
int NahidaProject::YAML::CountSpaces(const std::string& str, int tabSpaces)
{
	int spaces = 0;
	for (int i = 0; i < str.length(); i++)
	{
		if (str[i] == ' ')
			spaces++;
		else if (str[i] == '\t')
			spaces += tabSpaces;
		else
			return spaces;
	}
	return spaces;
}

// Removes tabs and spaces at the beginning and end of the string
// E.g. "   name: person " -> "name: person"
std::string NahidaProject::YAML::TruncateSpaces(const std::string& str)
{
	std::string new_str = "";

	int trailing_spaces = 0;

	for (int i = str.length() - 1; i >= 0; i--)
	{
		if (new_str.length() == 0 && (str[i] == ' ' || str[i] == '\t'))
		{
			trailing_spaces++;
			continue;
		}
		break;
	}

	for (int i = 0; i < str.length() - trailing_spaces; i++)
	{
		if (new_str.length() == 0 && (str[i] == ' ' || str[i] == '\t'))
			continue;
		new_str += str[i];
	}

	return new_str;
}

// Removes excess spaces and tabs that come after a line
std::string NahidaProject::YAML::TruncateSpacesAfter(const std::string& str)
{
	int trailing_spaces = 0;

	for (int i = str.length() - 1; i >= 0; i--)
	{
		if (str.length() == 0 && (str[i] == ' ' || str[i] == '\t'))
		{
			trailing_spaces++;
			continue;
		}
		break;
	}

	return str.substr(0, str.length() - trailing_spaces);
}

// Removes all tabs and spaces that come after a specified char
// E.g. "name:    person" -> "name:person"
std::string NahidaProject::YAML::RemoveSpacesAfterChar(const std::string& str, char c)
{
	std::string new_str = "";
	char last_char = '\0';
	for (int i = new_str.length(); i < str.length(); i++)
	{
		if (str[i] != ' ')
			last_char = str[i];
		if (last_char == c && str[i] == ' ')
			continue;
		new_str += str[i];
	}
	return new_str;
}

// Returns true if the YAML line is a variable-value pair, otherwise returns false
bool NahidaProject::YAML::IsVariableValuePair(const std::string& str)
{
	return (str.find(':') != (str.length() - 1));
}

// Remove a character completely from the given string
std::string NahidaProject::YAML::Filter(const std::string& str, char c)
{
	std::string new_str;
	for (int i = 0; i < str.length(); i++)
	{
		if (str[i] == c)
			continue;
		new_str += str[i];
	}
	return new_str;
}

std::array<std::string, 2> NahidaProject::YAML::SplitVariable(const std::string& str)
{
	std::string name, value;
	int delimiter_pos = str.find(':');
	name = str.substr(0, delimiter_pos);
	value = str.substr(delimiter_pos + 1, str.length());
	std::array<std::string, 2> name_value = { name, value };
	return name_value;
}

NahidaProject::YAML::YAMLType NahidaProject::YAML::GetType(const std::string& str){
	if (str.length() == 0)
		return YAMLType::noneType;

	if (str == std::string("true") || str == std::string("false"))
		return YAMLType::boolType;

	// Array
	if (str[0] == '[' && str[str.length() - 1] == ']')
		return YAMLType::arrayType;

	// Int
	bool is_int = true;
	for (int i = 0; i < str.length(); i++)
	{
		if ((str[i] < '0' || str[i] > '9') && str[i] != '-')
		{
			is_int = false;
			break;
		}
	}
	if (is_int)
		return YAMLType::intType;

	int number_of_dots = 0;
	bool contains_other_char = false;
	for (int i = 0; i < str.length(); i++){
		if ((str[i] < '0' || str[i] > '9') && str[i] != '.' && str[i] != '-'){
			contains_other_char = true;
			break;
		}
		if (str[i] == '.')
			number_of_dots++;
		if (number_of_dots > 1)
			break;
	}
	if (number_of_dots == 1 && !contains_other_char)
		return YAMLType::doubleType;

	return YAMLType::stringType;
}

std::vector<std::string> NahidaProject::YAML::SplitArray(const std::string& str){
	std::string tmp = "";
	int scope = 0;
	std::vector<std::string> tokens;
	for (int i = 0; i < str.length(); i++){
		if (str[i] == '[')
			scope++;
		else if (str[i] == ']')
			scope--;
		if ((str[i] == ',' || str[i] == ']') && scope <= 1){
			bool valid = true;
			tmp = tmp.substr(1, tmp.length() - 1);
			if (tmp.length() == 0)
				valid = false;
			if (tmp[0] == '[')
				tmp += ']';
			if (valid)
				tokens.push_back(tmp);
			tmp = "";
		}
		tmp += str[i];
	}
	return tokens;
}

NahidaProject::YAML::NamedValue NahidaProject::YAML::StringToValue(const std::string& str){
	NamedValue val;
	val.value = nullptr;

	std::array<std::string, 2> name_value = SplitVariable(str);
	YAMLType type = GetType(name_value[1]);

	val.type = type;
	val.name = name_value[0];

	if (type == YAMLType::noneType)
	{
		return val;
	}

	switch (type)
	{
	case YAMLType::intType:
		val.value = new int(std::stoi(name_value[1]));
		break;
	case YAMLType::doubleType:
		val.value = new double(std::stod(name_value[1]));
		break;
	case YAMLType::stringType:
		val.value = new std::string(name_value[1]);
		break;
	case YAMLType::arrayType:
	{
		val.value = new Array();
		std::string values_str = name_value[1];
		std::vector<std::string> values_split = SplitArray(values_str);
		for (int i = 0; i < values_split.size(); i++)
		{
			std::string named_val = "_:" + values_split.at(i);
			TypedValue new_val;
			NamedValue nval = StringToValue(values_split.at(i));
			new_val.type = nval.type;
			new_val.value = nval.value;
			static_cast<Array*>(val.value)->push_back(new_val);
		}
		break;
	}
	case YAMLType::boolType:
		val.value = new bool(name_value[1] == std::string("true"));
		break;
	}

	return val;
}

bool NahidaProject::YAML::IsValid(const NahidaProject::YAML::NamedValue& val){
	return val.value != nullptr;
}

NahidaProject::YAML::YAMLMap NahidaProject::YAML::Parse(std::string path) {
	YAMLMap yaml;
	std::vector<std::string> lines;

	int file_indent = 0;

	std::ifstream f(path);
	if (!f.is_open()) {
		std::cerr << "Encountered error while trying to open " << path << "\n";
		return yaml;
	}
	std::string tmp;
	while (getline(f, tmp))
	{
		tmp = tmp.substr(0, tmp.find('#'));

		if (TruncateSpaces(tmp).length() == 0)
			continue;

		int spaces = CountSpaces(tmp);
		if (spaces > 0 && file_indent == 0)
			file_indent = spaces;

		if (tmp.find(':') != std::string::npos)
		{
			lines.push_back(RemoveSpacesAfterChar(RemoveSpacesAfterChar(TruncateSpacesAfter(tmp), ':'), ','));
		}
		else
		{
			lines.at(lines.size() - 1) += RemoveSpacesAfterChar(RemoveSpacesAfterChar(TruncateSpaces(tmp), ':'), ',');
		}
	}
	f.close();

	std::stack<void*> scope_stack; // This is used for properly handling nested objects
	scope_stack.push(&yaml);
	int prev_indent = 0;

	for (int i = 0; i < lines.size(); i++)
	{
		int indent = CountSpaces(lines.at(i));

		if (indent < prev_indent)
		{
			for (int i = 0; i < (prev_indent - indent) / file_indent; i++)
			{
				scope_stack.pop();
			}
		}

		NamedValue v = StringToValue(TruncateSpaces(lines.at(i)));

		if (IsValid(v))
		{
			// Adding value to the deepest object
			(*((Object*)(scope_stack.top())))[v.name].value = v.value;
			(*((Object*)(scope_stack.top())))[v.name].type = v.type;
		}
		else
		{
			// Add a new object to the deepest object if the string has no value after the semicolon
			(*((Object*)(scope_stack.top())))[v.name] = TypedValue();
			(*((Object*)(scope_stack.top())))[v.name].type = YAMLType::objectType;
			(*((Object*)(scope_stack.top())))[v.name].value = new Object();
			scope_stack.push((*((Object*)(scope_stack.top())))[v.name].value);
		}

		prev_indent = indent;
	}

	return yaml;
}

void NahidaProject::YAML::DeleteValues(const NahidaProject::YAML::TypedValue& v){
	switch (v.type){
	case YAMLType::intType:
		delete static_cast<int*>(v.value);
		break;
	case YAMLType::doubleType:
		delete static_cast<double*>(v.value);
		break;
	case YAMLType::boolType:
		delete static_cast<bool*>(v.value);
		break;
	case YAMLType::stringType:
		delete static_cast<std::string*>(v.value);
		break;
	case YAMLType::arrayType:
	{
		for (int i = 0; i < (static_cast<Array*>(v.value))->size(); i++)
		{
			DeleteValues((static_cast<Array*>(v.value))->at(i));
		}
		delete static_cast<Array*>(v.value);
		break;
	}
	case YAMLType::objectType:
	{
		for (auto it : *(static_cast<Object*>(v.value)))
		{
			DeleteValues(it.second);
		}
		delete static_cast<Object*>(v.value);
		break;
	}
	}
}

NahidaProject::YAML::YAML::YAML(std::string path) {
	data = Parse(path);
}

NahidaProject::YAML::YAML::~YAML() {
	for (auto it : data) {
		DeleteValues(it.second);
	}
}

size_t NahidaProject::YAML::YAML::size() {
	return data.size();
}

NahidaProject::YAML::TypedValue NahidaProject::YAML::YAML::operator[](std::string name) {
	return data[name];
}

size_t NahidaProject::YAML::TypedValue::size() {
	switch (type) {
	case YAMLType::arrayType:
		return static_cast<std::vector<TypedValue>*>(value)->size();
	case YAMLType::objectType:
		return static_cast<std::unordered_map<std::string, TypedValue>*>(value)->size();
	}
	std::cerr << "ERROR: Unable to get length of type " << YAMLTypeToString(type) << ". This operation is only applicable to array and object.\n";
	return -1;
}